(ns cljsso.store
  (:require
   [taoensso.timbre :as timbre]
   [taoensso.carmine :as car :refer (wcar)]
   [sparrows.cypher :refer :all]
   [sparrows.misc :refer [if-and-let]]
   [cljsso.config :refer [props]]
   [clojure.string :as s]

   [cljsso.utils :refer [gen-uid-key find-host]]))

(timbre/refer-timbre)

(defonce rd-server
  (:sso-redis-conf
   props))

(defonce active-sessions
  (atom {}))


(defmacro wcar* [& body]
  `(car/wcar rd-server ~@body))



(defonce ticket-set
  "sso-ticket-set")

(defonce host-key-hash
  "sso-host-key-hash")

(defonce ticket-hash
  "sso-ticket-hash")

(defonce client-hosts
  "sso-client-hosts")

(defonce public-key
  (:sso-server-key props))

(defn add-host-key
  [host-id key]
  (wcar* (car/hset host-key-hash host-id key)))

(defn get-host-key
  "Get a host key"
  [host-id]
  (when host-id
    (wcar* (car/hget host-key-hash host-id))))


(def id->client-host
  (memoize
   (fn  [id]
     (let [clients (:sso-clients props)]
       (->
        (filter #(= (:id %) id) clients)
        first
        :host)))))

(def client-host->id
  (memoize
   (fn
     [host]
     (let [clients (:sso-clients props)]
       (->
        (filter #(= (:host %) host) clients)
        first
        :id)))))


(def id->client-logout-url
  (memoize
   (fn id->client-logout-url-intern
     [id]
     (let [clients (:sso-clients props)]
       (->
        (filter #(= (:id %) id) clients)
        first
        :logout-url)))))

(defn- try-decrypt
  "Try to decrypt the encrypted key, returns nil if exception is caught"
  [ticket key]
  (try
    (read-string (decrypt-aes ticket key))
    (catch Exception e
      (warn "Decrypt ticket:" ticket "failed with msg:" (.getMessage e)))))

(defn get-userinfo-from-ticket
  "Decrypt ticket and get userinfo"
  [ticket]
  (when ticket
    (try-decrypt ticket public-key))
)

(defn add-active-session
  "Add an uid and skey to `active-session` which is used to tracking
  user sessions among sso-clients"
  ([ticket service]
   (info "Adding session for" service "with ticket" ticket)
   (if-and-let
    [host               (find-host service)
     host-id            (client-host->id host)
     {:keys [uid skey]} (get-userinfo-from-ticket ticket)]
    (add-active-session uid skey host-id)))
  ([uid skey host-id]
   (info "Adding session for" "uid" uid "skey" skey "host" host-id)
   (swap! active-sessions
          update-in
          [(gen-uid-key uid skey)]
          #(conj (or % #{}) %2)
          host-id)))

(defn get-active-sessions
  "Get active host ids"
  [uid skey]
  (get @active-sessions (gen-uid-key uid skey)))


(defonce
  sso-clients-set
  (reduce
   #(conj % (:host %2))
   #{}
   (get-in props [:sso-clients])))



(defn init-host-keys
  "Initialize host keys. Keys will be loaded from config file, if not found,
  will assign random keys."
  []
  (doseq [{:keys [id key] :as host-map} (:sso-clients props)]
    (when-not (get-host-key id)
      (wcar* {car/hset client-hosts id host-map})
      (add-host-key  id (or key (md5 (str (System/currentTimeMillis) (rand-str 32))))))))




(defn check-ticket
  "Check if this ticket is valid"
  [ticket]
  (debug "Checking" ticket )
  (= 1 (wcar* (car/sismember ticket-set ticket))))


(defn invalidate-ticket
  [ticket]
  (wcar*
   (car/srem ticket-set ticket)))


(defn encrypt-userinfo
  "Encrypt userinfo using the key for this host "
  [skey userinfo]
  (encrypt-aes (str (assoc userinfo :skey skey)) public-key))


(defn gen-ticket
  "Create ticket for this user.
  Add this session to active-sessions if host-id is not nil. `userinfo`
  is an optional map which will be encrypted in ticket.
  "
  [{:keys [uid host-id userinfo]}]
  (if-let [key public-key]
    (let [userinfo (merge userinfo {:uid uid})
          skey     (rand-str 4)
          ticket   (encrypt-userinfo skey userinfo)]
      (if host-id (add-active-session uid skey host-id))
      (wcar* (car/sadd  ticket-set ticket))
      ticket)
    (error "Encryption key not found, make sure you have set `:sso-server-key`!")))
